﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using IMAPI2.Interop;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.ComponentModel;
using System.Windows;
using System.Windows.Threading;
using System.Threading;

namespace SoundServant
{
    public class Burner
    {
        // Iterop Declarations
        [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false, EntryPoint = "SHCreateStreamOnFileW")]
        static extern void SHCreateStreamOnFile(string fileName, uint mode, ref System.Runtime.InteropServices.ComTypes.IStream stream);

        private ArrayList queue = new ArrayList();
        private BurnerStatus status;
        private CD currentCD = null;
        private int currentTrackNumber = 0;

        private bool burnAtWill = true;
        private bool _cancel = false;

        DateTime burnStarted;
        MsftDiscMaster2 discMaster;
        IDiscRecorder2 device;
        MsftDiscFormat2TrackAtOnce audioCd;
        MsftDiscFormat2Data mp3CD;
        BackgroundWorker burner;



        public BurnerStatus Status { get { return status; } }
        public ArrayList Queue { get { return queue; } }
        public CD CurrentCD { get { return currentCD; } }
        public int CurrentTrackNumber { get { return currentTrackNumber; } }

        public delegate void VoidEventHandler();

        public delegate void ProgressEventHandler(int percent, string message);
        public delegate void FinishedEventHandler(TimeSpan timeTaken);
        public delegate void BurningTrackEventHandler(int track);
        public event ProgressEventHandler Progress;

        public event VoidEventHandler Starting;
        public event VoidEventHandler WaitingForCorrectMedia;
        public event VoidEventHandler Burning;
        public event VoidEventHandler Canceling;
        public event VoidEventHandler Canceled;
        public event VoidEventHandler Finishing;
        public event FinishedEventHandler Finished;
        public event VoidEventHandler Ready;
        public event VoidEventHandler QueueUpdated;
        public event VoidEventHandler NoMoreCDs;
        public event VoidEventHandler CDs;
        public event BurningTrackEventHandler BurningTrack;


        public Burner()
        {
            Initialize();
        }

        public void Initialize()
        {
            status = BurnerStatus.Ready;
            discMaster = new MsftDiscMaster2();
            device = new MsftDiscRecorder2();

            if (Options.Default.BurnerIndex + 1 > discMaster.Count) Options.Default.BurnerIndex = 0;
            try
            {
                device.InitializeDiscRecorder(discMaster[Options.Default.BurnerIndex]);
            }
            catch (Exception e)
            {
                Options.Default.BurnerIndex = 0;
                throw (new Exception("Fatal Error initialzing Disc Recorder:\r\n" + e.ToString() + "\r\n\r\nReseting Burner Index to 0"));
            }
            burner = new BackgroundWorker();
            burner.DoWork += new DoWorkEventHandler(BurnerThread);
            burner.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BurnerThreadFinished);
            Logs.Information("Burner", "Drive initialized - Id: " + device.ProductId + " Volume Path: " + device.VolumePathNames[0].ToString());
        }

        void BurnerThreadFinished(object sender, RunWorkerCompletedEventArgs e)
        {

        }

        public void AddCD(CD cd)
        {
            if (!cd.Disc.Error)
            {
                if (cd.Disc.Length < new TimeSpan(0, 74, 0))
                {
                    lock (queue)
                    { 
                    if (queue.Count == 0)
                    {
                        if (CDs != null) CDs();
                    }
                        queue.Add(cd);
                    }
                    QueueUpdated();
                    Start();
                }
                else
                {
                    SSMessageBox.Show("Error", "You cannont burn this CD because it is over 74 minutes long");
                }
            }
            else
            {
                SSMessageBox.Show("Error", "You cannont burn this CD because it contains errors");
            }
        }

        public void Start()
        {
            if (!burner.IsBusy && queue.Count > 0)
            {
                Logs.Information("Burner", "Starting");
                burner.RunWorkerAsync();
            }
        }

        private void BurnerThread(object sender, DoWorkEventArgs e)
        {
            if (queue.Count > 0)
            {
                // Remove next CD from the queue
                lock (queue)
                {
                    currentCD = (CD)queue[0];
                    queue.RemoveAt(0);
                }
                QueueUpdated();
                Logs.Information("Burner", "CD removed from queue");

                // Starting
                status = BurnerStatus.Starting;
                Starting();

                // Extract Disc From CD
                StoredDisc disc = currentCD.Disc;

                // Aquire Exclusive Access
                Logs.Information("Burner", "Starting burn of Audio CD - Recording Id: " + disc.Recording.Id + " Disc Id: " + disc.Id);
                Logs.Information("Burner", "Aquiring exclusive access");
                try
                {
                    device.AcquireExclusiveAccess(true, "SoundServant");
                }
                catch (COMException excep) { Logs.Error("Burner", "Failed to Aquire Exclusive Access to Drive", excep.ToString()); }
                catch (Exception excep) { Logs.Error("Burner", "Failed to Aquire Exclusive Access to Drive", excep.ToString()); }

                audioCd = new MsftDiscFormat2TrackAtOnce();
                audioCd.Update += new DiscFormat2TrackAtOnce_EventHandler(ProgressUpdate);
                audioCd.ClientName = "SoundServant";
                audioCd.Recorder = device;

                // Wait for Correct Media

                bool correctMediaLoaded = false;

                while (!correctMediaLoaded)
                {

                    // Point of no return
                    if (_cancel == true)
                    {
                        _cancel = false;
                        Thread.Sleep(new TimeSpan(0, 0, 2));
                        break;
                    }

                    try
                    {
                        if ((audioCd.CurrentPhysicalMediaType == IMAPI_MEDIA_PHYSICAL_TYPE.IMAPI_MEDIA_TYPE_CDR || audioCd.CurrentPhysicalMediaType == IMAPI_MEDIA_PHYSICAL_TYPE.IMAPI_MEDIA_TYPE_CDRW) && audioCd.MediaPhysicallyBlank == true)
                        {
                            correctMediaLoaded = true;
                            break;
                        }
                        else
                        {
                            if (status != BurnerStatus.Waiting) WaitingForCorrectMedia();
                            status = BurnerStatus.Waiting;
                        }
                    }
                    catch (COMException excep)
                    {
                        if (status != BurnerStatus.Waiting) WaitingForCorrectMedia();
                        status = BurnerStatus.Waiting;
                        Thread.Sleep(new TimeSpan(0, 0, 2));
                    }
                }

                if (correctMediaLoaded)
                {
                    // Burn Tracks
                    status = BurnerStatus.Burning;
                    Burning();
                    Logs.Information("Burner", "Preparing media");
                    burnStarted = DateTime.Now;

                    try
                    {
                        audioCd.PrepareMedia();
                    }
                    catch (COMException excep) { Logs.Error("Burner", "Failed to Prepare Media", excep.ToString()); }
                    catch (Exception excep) { Logs.Error("Burner", "Failed to Prepare Media", excep.ToString()); }

                    foreach (StoredTrack track in disc.Tracks)
                    {
                        Logs.Information("Burner", "Writing Track " + track.Number);
                        BurningTrack(track.Number);
                        currentTrackNumber = track.Number;
                        try
                        {
                            System.Runtime.InteropServices.ComTypes.IStream stream = null;
                            SHCreateStreamOnFile(track.File.FullName, 0x00000000 | 0x00000040, ref stream);
                            audioCd.AddAudioTrack(stream);
                        }
                        catch (COMException excep) { Logs.Error("Burner", "Audio CD Track Failed - Disc Id: " + disc.Recording.Id + " Track Id: " + track.Id, excep.ToString()); }
                        catch (Exception excep) { Logs.Error("Burner", "Audio CD Track Failed - Disc Id: " + disc.Recording.Id + " Track Id: " + track.Id, excep.ToString()); }
                        Logs.Information("Burner", "Track " + track.Number + " written");
                    }

                    // Finish CD
                    status = BurnerStatus.Finishing;
                    Finishing();
                    try
                    {
                        audioCd.ReleaseMedia();
                    }
                    catch (COMException excep) { Logs.Error("Burner", "Failed to Release Media", excep.ToString()); }
                    catch (Exception excep) { Logs.Error("Burner", "Failed to Release Media", excep.ToString()); }

                    currentCD.Disc.IncrementCopies();
                    TimeSpan timeTaken = DateTime.Now - burnStarted;
                    Logs.Information("Burner", "CD Burned Successfully: Recording Id: " + currentCD.Disc.Recording.FormatedDate + "(" + disc.Recording.Id + ") Disc: " + disc.Number + " (" + disc.Id + ") Time taken: (" + SS.ParseTimeSpan(timeTaken) + ")");

                    // Finished
                    try
                    {
                        device.EjectMedia();
                    }
                    catch (COMException excep) { Logs.Error("Burner", "Failed to Eject Media Media", excep.ToString()); }
                    catch (Exception excep) { Logs.Error("Burner", "Failed to Eject Media", excep.ToString()); }
                    Finished(timeTaken);

                }
                else
                {
                    Canceled();
                }

                try
                {
                    device.ReleaseExclusiveAccess();

                }
                catch (COMException excep) { Logs.Error("Burner", "Failed to Release Exclusive Access", excep.ToString()); }
                catch (Exception excep) { Logs.Error("Burner", "Failed to Release Access", excep.ToString()); }

                currentCD.Done();
                currentCD = null;
                Thread.Sleep(new TimeSpan(0, 0, 1));
                Logs.Information("Burner", "Returning burner to ready state");
                status = BurnerStatus.Ready;
                Ready();
            }
        }

        void ProgressUpdate(object sender, object progress)
        {
            string message = "";
            int percent = 0;
            IDiscFormat2TrackAtOnceEventArgs eventArgs = (IDiscFormat2TrackAtOnceEventArgs)progress;

            switch (eventArgs.CurrentAction)
            {
                case IMAPI_FORMAT2_TAO_WRITE_ACTION.IMAPI_FORMAT2_TAO_WRITE_ACTION_PREPARING:
                    message += "Preparing Disc";
                    break;

                case IMAPI_FORMAT2_TAO_WRITE_ACTION.IMAPI_FORMAT2_TAO_WRITE_ACTION_UNKNOWN:
                    message += "....";
                    break;
                case IMAPI_FORMAT2_TAO_WRITE_ACTION.IMAPI_FORMAT2_TAO_WRITE_ACTION_WRITING:
                    long writtenSectors = eventArgs.LastWrittenLba - eventArgs.StartLba;


                    if (writtenSectors > 0 && eventArgs.SectorCount > 0)
                    {
                        message += "Burning Track " + eventArgs.CurrentTrackNumber + "/" + currentCD.Disc.Tracks.Count.ToString() + " (-" + (eventArgs.RemainingTime / 60).ToString("00") + ":" + (eventArgs.RemainingTime % 60).ToString("00") + ")";
                        percent = (int)((100 * writtenSectors) / eventArgs.SectorCount);
                    }
                    else
                    {
                        message = "...";
                        percent = 0;
                    }
                    break;
                case IMAPI_FORMAT2_TAO_WRITE_ACTION.IMAPI_FORMAT2_TAO_WRITE_ACTION_FINISHING:
                    message += "Finishing Disc";
                    break;

            }

            Progress(percent, message);
        }

        public void CancelCurrentCD()
        {
            if (status == BurnerStatus.Starting || status == BurnerStatus.Waiting)
            {
                Logs.Information("Burner", "Canceling current CD");
                Canceling();
                _cancel = true;
            }
        }

        public void CancelAllCDs()
        {
            
            lock (queue)
            {
                    foreach (CD cd in queue)
                    {
                        cd.Done();
                    }
                    queue.Clear();
            }
                QueueUpdated();

                if (status == BurnerStatus.Ready)
                {
                    NoMoreCDs();
                }
            

        }

        public void CancelCD(CD cdToCancel)
        {
            if (queue.Contains(cdToCancel))
            {
                Logs.Information("Burner", "Removing CD from Queue - For Who: " + cdToCancel.ForWho);
                cdToCancel.Done();
                queue.Remove(cdToCancel);
                QueueUpdated();
            }
        }
    }

    public enum BurnerStatus
    {
        Ready,
        Starting,
        Waiting,
        Burning,
        Finishing,
        Canceling
    }
}
